package org.andengine.opengl.texture.compressed.pvr;

import android.opengl.GLES20;

import org.andengine.BuildConfig;
import org.andengine.opengl.texture.ITextureStateListener;
import org.andengine.opengl.texture.PixelFormat;
import org.andengine.opengl.texture.Texture;
import org.andengine.opengl.texture.TextureManager;
import org.andengine.opengl.texture.TextureOptions;
import org.andengine.opengl.texture.compressed.pvr.pixelbufferstrategy.GreedyPVRTexturePixelBufferStrategy;
import org.andengine.opengl.texture.compressed.pvr.pixelbufferstrategy.IPVRTexturePixelBufferStrategy;
import org.andengine.opengl.texture.compressed.pvr.pixelbufferstrategy.IPVRTexturePixelBufferStrategy.IPVRTexturePixelBufferStrategyBufferManager;
import org.andengine.opengl.util.GLState;
import org.andengine.util.StreamUtils;
import org.andengine.util.adt.DataConstants;
import org.andengine.util.adt.array.ArrayUtils;
import org.andengine.util.adt.io.out.ByteBufferOutputStream;
import org.andengine.util.debug.Debug;
import org.andengine.util.math.MathUtils;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
 * (c) 2010 Nicolas Gramlich
 * (c) 2011 Zynga Inc.
 *
 * @author Nicolas Gramlich
 * @since 16:18:10 - 13.07.2011
 */
public abstract class PVRTexture extends Texture {
    // ===========================================================
    // Constants
    // ===========================================================

    public static final int FLAG_MIPMAP = (1 << 8); // has mip map levels
    public static final int FLAG_TWIDDLE = (1 << 9); // is twiddled
    public static final int FLAG_BUMPMAP = (1 << 10); // has normals encoded for a bump map
    public static final int FLAG_TILING = (1 << 11); // is bordered for tiled pvr
    public static final int FLAG_CUBEMAP = (1 << 12); // is a cubemap/skybox
    public static final int FLAG_FALSEMIPCOL = (1 << 13); // are there false colored MIP levels
    public static final int FLAG_VOLUME = (1 << 14); // is this a volume texture
    public static final int FLAG_ALPHA = (1 << 15); // v2.1 is there transparency info in the texture
    public static final int FLAG_VERTICALFLIP = (1 << 16); // v2.1 is the texture vertically flipped

    // ===========================================================
    // Fields
    // ===========================================================

    private final PVRTextureHeader mPVRTextureHeader;
    private final IPVRTexturePixelBufferStrategy mPVRTexturePixelBufferStrategy;

    // ===========================================================
    // Constructors
    // ===========================================================

    public PVRTexture(final TextureManager pTextureManager, final PVRTextureFormat pPVRTextureFormat) throws IllegalArgumentException, IOException {
        this(pTextureManager, pPVRTextureFormat, new GreedyPVRTexturePixelBufferStrategy(), TextureOptions.DEFAULT, null);
    }

    public PVRTexture(final TextureManager pTextureManager, final PVRTextureFormat pPVRTextureFormat, final IPVRTexturePixelBufferStrategy pPVRTexturePixelBufferStrategy) throws IllegalArgumentException, IOException {
        this(pTextureManager, pPVRTextureFormat, pPVRTexturePixelBufferStrategy, TextureOptions.DEFAULT, null);
    }

    public PVRTexture(final TextureManager pTextureManager, final PVRTextureFormat pPVRTextureFormat, final ITextureStateListener pTextureStateListener) throws IllegalArgumentException, IOException {
        this(pTextureManager, pPVRTextureFormat, new GreedyPVRTexturePixelBufferStrategy(), TextureOptions.DEFAULT, pTextureStateListener);
    }

    public PVRTexture(final TextureManager pTextureManager, final PVRTextureFormat pPVRTextureFormat, final IPVRTexturePixelBufferStrategy pPVRTexturePixelBufferStrategy, final ITextureStateListener pTextureStateListener) throws IllegalArgumentException, IOException {
        this(pTextureManager, pPVRTextureFormat, pPVRTexturePixelBufferStrategy, TextureOptions.DEFAULT, pTextureStateListener);
    }

    public PVRTexture(final TextureManager pTextureManager, final PVRTextureFormat pPVRTextureFormat, final TextureOptions pTextureOptions) throws IllegalArgumentException, IOException {
        this(pTextureManager, pPVRTextureFormat, new GreedyPVRTexturePixelBufferStrategy(), pTextureOptions, null);
    }

    public PVRTexture(final TextureManager pTextureManager, final PVRTextureFormat pPVRTextureFormat, final IPVRTexturePixelBufferStrategy pPVRTexturePixelBufferStrategy, final TextureOptions pTextureOptions) throws IllegalArgumentException, IOException {
        this(pTextureManager, pPVRTextureFormat, pPVRTexturePixelBufferStrategy, pTextureOptions, null);
    }

    public PVRTexture(final TextureManager pTextureManager, final PVRTextureFormat pPVRTextureFormat, final TextureOptions pTextureOptions, final ITextureStateListener pTextureStateListener) throws IllegalArgumentException, IOException {
        this(pTextureManager, pPVRTextureFormat, new GreedyPVRTexturePixelBufferStrategy(), pTextureOptions, pTextureStateListener);
    }

    public PVRTexture(final TextureManager pTextureManager, final PVRTextureFormat pPVRTextureFormat, final IPVRTexturePixelBufferStrategy pPVRTexturePixelBufferStrategy, final TextureOptions pTextureOptions, final ITextureStateListener pTextureStateListener) throws IllegalArgumentException, IOException {
        super(pTextureManager, pPVRTextureFormat.getPixelFormat(), pTextureOptions, pTextureStateListener);
        this.mPVRTexturePixelBufferStrategy = pPVRTexturePixelBufferStrategy;

        InputStream inputStream = null;
        try {
            inputStream = this.getInputStream();
            this.mPVRTextureHeader = new PVRTextureHeader(StreamUtils.streamToBytes(inputStream, PVRTextureHeader.SIZE));
        } finally {
            StreamUtils.close(inputStream);
        }

        if (this.mPVRTextureHeader.getPVRTextureFormat().getPixelFormat() != pPVRTextureFormat.getPixelFormat()) {
            throw new IllegalArgumentException("Other PVRTextureFormat: '" + this.mPVRTextureHeader.getPVRTextureFormat().getPixelFormat() + "' found than expected: '" + pPVRTextureFormat.getPixelFormat() + "'.");
        }

        if (this.mPVRTextureHeader.getPVRTextureFormat().isCompressed()) { // TODO && ! GLHELPER_EXTENSION_PVRTC] ) {
            throw new IllegalArgumentException("Invalid PVRTextureFormat: '" + this.mPVRTextureHeader.getPVRTextureFormat() + "'.");
        }

        if (this.hasMipMaps()) {
            switch (pTextureOptions.mMinFilter) {
                case GLES20.GL_NEAREST_MIPMAP_NEAREST:
                case GLES20.GL_NEAREST_MIPMAP_LINEAR:
                case GLES20.GL_LINEAR_MIPMAP_NEAREST:
                case GLES20.GL_LINEAR_MIPMAP_LINEAR:
                    break;
                default:
                    if (BuildConfig.DEBUG) {
                        Debug.w("This '" + this.getClass().getSimpleName() + "' contains mipmaps, but the provided '" + pTextureOptions.getClass().getSimpleName() + "' don't have MipMaps enabled on the MinFilter!");
                    }
            }
        }

        this.mUpdateOnHardwareNeeded = true;
    }

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    @Override
    public int getWidth() {
        return this.mPVRTextureHeader.getWidth();
    }

    @Override
    public int getHeight() {
        return this.mPVRTextureHeader.getHeight();
    }

    public boolean hasMipMaps() {
        return this.mPVRTextureHeader.getNumMipmaps() > 0;
    }

    public PVRTextureHeader getPVRTextureHeader() {
        return this.mPVRTextureHeader;
    }

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================

    protected abstract InputStream onGetInputStream() throws IOException;

    public InputStream getInputStream() throws IOException {
        return this.onGetInputStream();
    }

    @Override
    protected void writeTextureToHardware(final GLState pGLState) throws IOException {
        final IPVRTexturePixelBufferStrategyBufferManager pvrTextureLoadStrategyManager = this.mPVRTexturePixelBufferStrategy.newPVRTexturePixelBufferStrategyManager(this);

        int width = this.getWidth();
        int height = this.getHeight();

        final int dataLength = this.mPVRTextureHeader.getDataLength();

        final int bytesPerPixel = this.mPVRTextureHeader.getBitsPerPixel() / DataConstants.BITS_PER_BYTE;

		/* Adjust unpack alignment. */
        GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);

        int currentLevel = 0;
        int currentPixelDataOffset = 0;
        while (currentPixelDataOffset < dataLength) {
            if (currentLevel > 0 && (width != height || MathUtils.nextPowerOfTwo(width) != width)) {
                Debug.w("Mipmap level '" + currentLevel + "' is not squared. Width: '" + width + "', height: '" + height + "'. Texture won't render correctly.");
            }

            final int currentPixelDataSize = height * width * bytesPerPixel;

			/* Load the current level. */
            this.mPVRTexturePixelBufferStrategy.loadPVRTextureData(pvrTextureLoadStrategyManager, width, height, bytesPerPixel, this.mPixelFormat, currentLevel, currentPixelDataOffset, currentPixelDataSize);

            currentPixelDataOffset += currentPixelDataSize;

			/* Prepare next mipmap level. */
            width = Math.max(width / 2, 1);
            height = Math.max(height / 2, 1);

            currentLevel++;
        }

		/* Restore default unpack alignment. */
        GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, GLState.GL_UNPACK_ALIGNMENT_DEFAULT);
    }

    // ===========================================================
    // Methods
    // ===========================================================

    public ByteBuffer getPVRTextureBuffer() throws IOException {
        final InputStream inputStream = this.getInputStream();
        try {
            final ByteBufferOutputStream os = new ByteBufferOutputStream(DataConstants.BYTES_PER_KILOBYTE, DataConstants.BYTES_PER_MEGABYTE / 2);
            StreamUtils.copy(inputStream, os);
            return os.toByteBuffer();
        } finally {
            StreamUtils.close(inputStream);
        }
    }

    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================

    public static class PVRTextureHeader {
        // ===========================================================
        // Constants
        // ===========================================================

        static final byte[] MAGIC_IDENTIFIER = {
                (byte) 'P',
                (byte) 'V',
                (byte) 'R',
                (byte) '!'
        };

        public static final int SIZE = 13 * DataConstants.BYTES_PER_INT;
        private static final int FORMAT_FLAG_MASK = 0x0FF;

        // ===========================================================
        // Fields
        // ===========================================================

        private final ByteBuffer mDataByteBuffer;
        private final PVRTextureFormat mPVRTextureFormat;

        // ===========================================================
        // Constructors
        // ===========================================================

        public PVRTextureHeader(final byte[] pData) {
            this.mDataByteBuffer = ByteBuffer.wrap(pData);
            this.mDataByteBuffer.rewind();
            this.mDataByteBuffer.order(ByteOrder.LITTLE_ENDIAN);

			/* Check magic bytes. */
            if (!ArrayUtils.equals(pData, 11 * DataConstants.BYTES_PER_INT, PVRTextureHeader.MAGIC_IDENTIFIER, 0, PVRTextureHeader.MAGIC_IDENTIFIER.length)) {
                throw new IllegalArgumentException("Invalid " + this.getClass().getSimpleName() + "!");
            }

            this.mPVRTextureFormat = PVRTextureFormat.fromID(this.getFlags() & PVRTextureHeader.FORMAT_FLAG_MASK);
        }

        // ===========================================================
        // Getter & Setter
        // ===========================================================

        public PVRTextureFormat getPVRTextureFormat() {
            return this.mPVRTextureFormat;
        }

        public int headerLength() {
            return this.mDataByteBuffer.getInt(0 * DataConstants.BYTES_PER_INT); // TODO Constants
        }

        public int getHeight() {
            return this.mDataByteBuffer.getInt(1 * DataConstants.BYTES_PER_INT);
        }

        public int getWidth() {
            return this.mDataByteBuffer.getInt(2 * DataConstants.BYTES_PER_INT);
        }

        public int getNumMipmaps() {
            return this.mDataByteBuffer.getInt(3 * DataConstants.BYTES_PER_INT);
        }

        public int getFlags() {
            return this.mDataByteBuffer.getInt(4 * DataConstants.BYTES_PER_INT);
        }

        public int getDataLength() {
            return this.mDataByteBuffer.getInt(5 * DataConstants.BYTES_PER_INT);
        }

        public int getBitsPerPixel() {
            return this.mDataByteBuffer.getInt(6 * DataConstants.BYTES_PER_INT);
        }

        public int getBitmaskRed() {
            return this.mDataByteBuffer.getInt(7 * DataConstants.BYTES_PER_INT);
        }

        public int getBitmaskGreen() {
            return this.mDataByteBuffer.getInt(8 * DataConstants.BYTES_PER_INT);
        }

        public int getBitmaskBlue() {
            return this.mDataByteBuffer.getInt(9 * DataConstants.BYTES_PER_INT);
        }

        public int getBitmaskAlpha() {
            return this.mDataByteBuffer.getInt(10 * DataConstants.BYTES_PER_INT);
        }

        public boolean hasAlpha() {
            return this.getBitmaskAlpha() != 0;
        }

        public int getPVRTag() {
            return this.mDataByteBuffer.getInt(11 * DataConstants.BYTES_PER_INT);
        }

        public int numSurfs() {
            return this.mDataByteBuffer.getInt(12 * DataConstants.BYTES_PER_INT);
        }

        // ===========================================================
        // Methods for/from SuperClass/Interfaces
        // ===========================================================

        // ===========================================================
        // Methods
        // ===========================================================

        // ===========================================================
        // Inner and Anonymous Classes
        // ===========================================================
    }

    public static enum PVRTextureFormat {
        // ===========================================================
        // Elements
        // ===========================================================

        RGBA_4444(0x10, false, PixelFormat.RGBA_4444),
        RGBA_5551(0x11, false, PixelFormat.RGBA_5551),
        RGBA_8888(0x12, false, PixelFormat.RGBA_8888),
        RGB_565(0x13, false, PixelFormat.RGB_565),
        //		RGB_555( 0x14, ...),
        //		RGB_888( 0x15, ...),
        I_8(0x16, false, PixelFormat.I_8),
        AI_88(0x17, false, PixelFormat.AI_88),
        //		PVRTC_2(0x18, GL10.GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG, true, TextureFormat.???),
        //		PVRTC_4(0x19, GL10.GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG, true, TextureFormat.???),
        //		BGRA_8888(0x1A, GL10.GL_RGBA, TextureFormat.???),
        A_8(0x1B, false, PixelFormat.A_8);

        // ===========================================================
        // Constants
        // ===========================================================

        // ===========================================================
        // Fields
        // ===========================================================

        private final int mID;
        private final boolean mCompressed;
        private final PixelFormat mPixelFormat;

        // ===========================================================
        // Constructors
        // ===========================================================

        private PVRTextureFormat(final int pID, final boolean pCompressed, final PixelFormat pPixelFormat) {
            this.mID = pID;
            this.mCompressed = pCompressed;
            this.mPixelFormat = pPixelFormat;
        }

        public static PVRTextureFormat fromID(final int pID) {
            final PVRTextureFormat[] pvrTextureFormats = PVRTextureFormat.values();
            final int pvrTextureFormatCount = pvrTextureFormats.length;
            for (int i = 0; i < pvrTextureFormatCount; i++) {
                final PVRTextureFormat pvrTextureFormat = pvrTextureFormats[i];
                if (pvrTextureFormat.mID == pID) {
                    return pvrTextureFormat;
                }
            }
            throw new IllegalArgumentException("Unexpected " + PVRTextureFormat.class.getSimpleName() + "-ID: '" + pID + "'.");
        }

        public static PVRTextureFormat fromPixelFormat(final PixelFormat pPixelFormat) throws IllegalArgumentException {
            switch (pPixelFormat) {
                case RGBA_8888:
                    return PVRTextureFormat.RGBA_8888;
                case RGBA_4444:
                    return PVRTextureFormat.RGBA_4444;
                case RGB_565:
                    return PVRTextureFormat.RGB_565;
                default:
                    throw new IllegalArgumentException("Unsupported " + PixelFormat.class.getName() + ": '" + pPixelFormat + "'.");
            }
        }

        // ===========================================================
        // Getter & Setter
        // ===========================================================

        public int getID() {
            return this.mID;
        }

        public boolean isCompressed() {
            return this.mCompressed;
        }

        public PixelFormat getPixelFormat() {
            return this.mPixelFormat;
        }

        // ===========================================================
        // Methods from SuperClass/Interfaces
        // ===========================================================

        // ===========================================================
        // Methods
        // ===========================================================

        // ===========================================================
        // Inner and Anonymous Classes
        // ===========================================================
    }
}